<?php

namespace StudyBuddy;

/**
 * Object representation of string
 *
 */
class String extends StudyBuddyObject implements \Serializable {

    /**
     * The string that this object represents
     * @var string
     */
    protected $string;

    /**
     * This mode indicates how
     * we return the result
     * when the result is a new string
     *
     * The default is 'immutable' mode
     * which means that a new object of this class
     * is created for the new string and returned
     *
     * Another option is to set it
     * to 'StringBuilder' which simulates the Java StringBuilder object
     * in which case when string changes, only the
     * instance of $this->string is changing (object is not immutable in
     * this case) and $this is returned
     *
     * The StringBuilder is more effecient because it does not
     * create a new object every time the string changes
     *
     * Default is 'immutable'
     * @var string
     */
    protected $returnMode = 'default';

    /**
     * Constructor
     * @param string $string
     * @param string $returnMode if set to StringBuilder will set the StringBuilder return mode
     */
    public function __construct($string, $returnMode = 'default') {
        if (!\is_string($string) && !\is_int($string) && !\is_object($string)) {
            $err = '$string must be a string of int. Was: ' . gettype($string);
            e($err);
            throw new \InvalidArgumentException($err);
        }

        if (\is_object($string)) {

            if (!($string instanceof \StudyBuddy\String)) {
                $err = '$string must be instance of \StudyBuddy\String class';
                e($err);
                throw new \InvalidArgumentException($err);
            }

            $string = $string->valueOf();
        }

        d('cp');

        $this->string = (string) $string;
        //d('cp');
        $this->returnMode = $returnMode;
        //d('cp');
    }

    /**
     * Setter for returnMode
     * Should be default or StringBuilder
     * but basically if it's any string other
     * that StringBuilder, it will set the return
     * mode to default, making this an immutable object
     *
     * @param $mode
     * @return object $this
     */
    public function setReturnMode($mode) {
        if (!is_string($mode)) {
            throw new InvalidArgumentException('$mode must be a string');
        }

        $this->returnMode = $mode;

        return $this;
    }

    /**
     * Getter for returnMode
     *
     * @return string
     */
    public function getReturnMode() {
        return $this->returnMode;
    }

    /**
     *
     * factory method but cannot call it factory
     * due to some bug in certain php versions
     * that raize error if static method signature
     * is not the same as in parent class
     * @param string $string
     */
    public static function factory($string) {

        $o = new \StudyBuddy\String($string);

        return $o;
    }

    /**
     * @todo unfinished
     *
     */
    public function __clone() {
        
    }

    /**
     * (non-PHPdoc)
     * @see StudyBuddy.StudyBuddyObject::__toString()
     */
    public function __toString() {
        return $this->string;
    }

    /**
     *
     * @return string value of $this->string
     */
    public function valueOf() {
        return $this->string;
    }

    /**
     * (non-PHPdoc)
     * @see StudyBuddy.StudyBuddyObject::hashCode()
     */
    public function hashCode() {
        return $this->getMd5();
    }

    /**
     * Tests to see if the string
     * contains html
     *
     * @return bool true if string contains html tags, false otherwise
     */
    public function isHtml() {

        return (\strlen(\strip_tags($this->string)) !== \strlen($this->string));
    }

    /**
     * (non-PHPdoc)
     * @see Serializable::serialize()
     */
    public function serialize() {
        return \serialize(array('s' => $this->string, 'm' => $this->returnMode));
    }

    /**
     * (non-PHPdoc)
     * @see Serializable::unserialize()
     */
    public function unserialize($serialized) {
        $a = \unserialize($serialized);
        $this->string = $a['s'];
        $this->returnMode = $a['m'];
    }

    /**
     * Returnes number of lines in a string
     * This is utf-8 safe,
     * so no need to override in utf8 string class
     *
     * @return int number of lines
     */
    public function getLinesCount() {

        $a = \explode("\n", trim($this->string, "\n"));

        return count($a);
    }

    /**
     * Counts number of words in a string
     *
     * @param string $str
     * @return int number of words in a string
     */
    public function getWordsCount() {

        return \str_word_count($this->string, 0, '0123456789-');
    }

    /**
     * Return number of sentences in a string
     * sentense is spotted by the end-of-sentence
     * punctuation mark: ., !, or ?
     * that is NOT followed by a word.
     *
     * @param $str
     * @return int number of sentences in a string
     */
    public function getSentencesCount() {

        return \preg_match_all('/(?:[\w])([\.!?])(?!\w)/m', $this->string, $match);
    }

    /**
     * Get string length in bytes
     * It is important to understand that
     * for normal ASCII string this is also the number
     * of chars in string, but for utf-8 this may be
     * different. That's why this method whould be
     * overritten in Utf8String class
     *
     * @return int length of string in bytes
     *
     */
    public function length() {

        return \strlen($this->string);
    }

    /**
     *
     * @return md5 hash of $this->string
     */
    public function getMd5() {
        return \md5($this->string);
    }

    /**
     *
     * @return crc32 value of this string
     */
    public function getCrc32() {
        return \crc32($this->string);
    }

    /**
     * Remove (mask) some chars from email address
     * so that it becomes not valid for physhers
     * and can be displayed on the web page
     *
     * @param object of this type
     */
    public function obfuscateEmail() {
        $str = \preg_replace('/([a-zA-Z0-9_\.]{2,})(@)/Ume', "substr('\\1', 0, rand((floor(strlen('\\1') / 2)), (floor(strlen('\\1') / 2) + 1))).'### @'", $this->string);

        return $this->handleReturn($str);
    }

    /**
     * Strip tags but preserve white spaces
     *
     * @return object of this class representing
     * new string
     */
    public function asPlainText() {

        if (!$this->isHtml()) {

            return $this->handleReturn($this->string);
        }

        /**
         * Remove all the < brackets with space
         * so that when tags are stripped we will
         * not lose any spaces
         */
        $text = \str_replace('<', ' <', $this->string);
        $text = \strip_tags($text, '<m>');
        $text = \preg_replace('/[\n\r\t]+/', ' ', $text);
        $text = \preg_replace('!\s+!', ' ', $text);

        return $this->handleReturn(\trim($text));
    }

    public function stripTags(array $aAllowed = null) {
        $ret = \strip_tags($this->string, $aAllowed);

        return $this->handleReturn($ret);
    }

    /**
     * Generates random alphanumeric string
     * of predetermined length
     *
     * @param intered $len
     * @return string a string of random letters and numbers.
     *                letters in the string randomly using upper
     *                or lower case
     */
    public static function makeRandomString($len = 30) {

        $strAlphanum = 'abcdefghijklmnopqrstuvwqyz0123456789';
        $len = (int) $len;
        $aAlphanum = \str_split($strAlphanum);
        $strRes = '';
        for ($i = 0; $i < $len; $i += 1) {
            $key = \mt_rand(0, 35);
            $char = $aAlphanum[$key];
            $strRes .= (1 === \mt_rand(0, 1) && !\is_numeric($char)) ? \strtoupper($char) : $char;
        }

        return $strRes;
    }

    /**
     * Make random string that will be used
     * as value of 'sid' cookie
     * The value is generated based on current microtime
     * so the string always starts with current microtime,
     * then the letter a and remainder is a random string
     * with the length necessary to make the total length
     * equal to $len param
     *
     * @param int $len the total length of generated sid
     * @return string random string of $len chars
     * the string always starts with microtime value
     * then char 'a' and then random string
     * This way we can always extract the microtime
     * from it and find out when user first got this cookie
     * meaning we don't even need to set a separete 'first visit'
     * cookie!
     *
     */
    public static function makeSid($len = 48) {
        $prefix = \microtime(true) . 'a';
        $rs = self::makeRandomString($len - \strlen($prefix));

        return $prefix . $rs;
    }

    /**
     * Returnes sha256 hashed password
     * using STUDYBUDDY_SALT from settings as salt
     * @param string $pwd password to hash
     * @return string md5 hash of VERSION + $pwd
     */
    public static function hashPassword($pwd) {
        $salt = STUDYBUDDY_SALT;

        return \hash('sha256', $salt . $pwd);
    }

    /**
     * Create random number on 6 to 8 digits long
     * This is useful to generate initial password
     * for a new user
     *
     * This method will also make sure that password
     * has at least one number character in it in order
     * to pass password validation which requires the password
     * to have letters and numbers
     *
     * @return string a randomly generate password
     * of length between $minLen and $maxLen
     *
     */
    public static function makePasswd($minLen = 6, $maxLen = 8) {
        $len = \mt_rand($minLen, $maxLen);
        d('len: ' . $len);
        $pwd = self::makeRandomString($len);


        /**
         * if result string does not have at least one number
         * then append one digit to the end of string
         * this is so that the password will pass
         * the validator during login process,
         * which requires the password to have at least one digit
         */
        if (0 === \preg_match('/\d/', $pwd)) {
            $digit = \mt_rand(0, 9);
            $pwd .= $digit;

            /**
             * Now that we added an extra char
             * to end of string, we must remove
             * the first char to keep the string to be
             * not over the maxLen value
             *
             * But only if it exceeded $maxLen
             */
            if (strlen($pwd) > $maxLen) {
                $pwd = \substr($pwd, 1);
            }
        }

        return $pwd;
    }

    /**
     * Wraps the string inside this html (or xml) tag
     *
     * @param string $tag defaults to <pre>
     * MUST specify tag without the < > brackets, just a tag name
     * for example 'div'
     *
     * @return object of this class (new object or $this)
     */
    public function wrapInTag($tag = 'pre') {
        $string = '<' . $tag . '>' . $this->string . '</' . $tag . '>';

        return $this->handleReturn($string);
    }

    /**
     * Handles the return function
     * depending on returnMode it will either
     * modify the value of $this->string and return $this
     * OR will create a new object of this class for the
     * new string and return that new object
     * Either way an object of this class is returned
     *
     * @param $string
     *
     * @return object of this class
     */
    protected function handleReturn($string) {

        if ('StringBuilder' !== $this->returnMode) {
            $o = new static($string, $this->returnMode);
            return $o;
        }

        $this->string = $string;

        return $this;
    }

    /**
     * A simpler implementation of linkify
     * does not do truncating of link text
     * but seems to work better for certain links
     *
     * @important DO NOT use on HTML String!
     * for html string use HTMLStringParser::linkify()
     *
     * @return object of this class
     */
    public function linkify() {
        if ($this->isHtml()) {
            e('not cool to linkify this string because it is an HTML string Use \StudyBuddy\String\HTMLStringParser::linkify() for HTML strings');
        }

        $text = $this->string;
        $text = \preg_replace("/(^|[\n ])([\w]*?)((ht|f)tp(s)?:\/\/[\w]+[^ \,\"\n\r\t<]*)/is", "$1$2<a href=\"$3\" rel=\"nofollow\">$3</a>", $text);
        $text = \preg_replace("/(^|[\n ])([\w]*?)((www|ftp)\.[^ \,\"\t\n\r<]*)/is", "$1$2<a href=\"http://$3\" rel=\"nofollow\">$3</a>", $text);

        return $this->handleReturn($text);
    }

    /**
     * Cut string of text to be not more
     * than $max chars but makes sure it cuts
     * only on word boundary
     *
     * @param int $max
     * @param string $link
     * @return object of this class
     */
    public function truncate($max, $link = '') {
        preg_match_all("|<m>(.*?)</m>|", $this->string, $regs, PREG_SET_ORDER);
        if ($regs) {
            preg_replace("/<m>(.*?)<\/m>/", " QA_MATH_EQUATION ", $this->string);
        }
        $words = \preg_split("/[\s]+/", $this->string);

        $newstring = '';
        $numwords = 0;
        $index = 0;

        foreach ($words as $word) {
            if ((\strlen($newstring) + 1 + \strlen($word)) < $max) {
                if ($word == 'QA_MATH_EQUATION') {
                    $word = '<m>' . $regs[$index][0] . '</m>';
                    $index++;
                }
                $newstring .= ' ' . $word;
                ++$numwords;
            } else {
                break;
            }
        }

        if ($numwords < count($words)) {
            $newstring .= '... ' . $link;
        }

        return $this->handleReturn(\trim($newstring));
    }

    /**
     * Change the & to $amp;
     * but only if the & is not part of already encoded
     * html entity, including &amp;
     * This is most likely utf8 safe because the pattern
     * only contains valid ascii chars
     * and just in case, we also use the /u swtich with
     * will make the replace fail if input contains invalid utf8 chars
     *
     * @return object of this class
     */
    public function escapeAmp() {
        $newstring = \preg_replace('/&(?!([#]{0,1})([a-zA-Z0-9]{2,9});)/u', '&amp;', $this->string);

        return $this->handleReturn($newstring);
    }

    /**
     * Replace non alphanumerics with underscores,
     * limit to 65 chars
     *
     * @return object of this class
     * representing the new string which is suitable
     * for being part of a url (SEO-friendly)
     */
    public function makeLinkTitle($limit = 65) {
        /**
         * Remove 'a', 'the', 'an', 'i', 'you', 'we', 'it', 'is', 'are'
         */
        $aFiltered = array(
            '/ a /i',
            '/ the /i',
            '/ an /i',
            '/ i /i',
            '/ am /i',
            '/ you /i',
            '/ we /i',
            '/ it /i',
            '/ is /i',
            '/ are /i',
            '/ of /i',
            '/ i\'m /i',
            '/ it\'s /i',
            '/ of /i',
            '/ of /i',
            '/ my /i'
        );

        $str = \preg_replace($aFiltered, ' ', $this->string);

        /**
         * All non-alpha numeric chars will become dashes
         */
        $str = \preg_replace('/([^a-zA-Z0-9\-_])/', '-', $str);

        $str = \preg_replace('/(-){2,}/', '-', $str);

        /**
         * Remove the Re: type prefix
         * because it's not adding any value to
         * SEO-freindly string
         */
        $str = \preg_replace('/^re-/i', '', $str);

        /**
         * Replace anything that looks like -_ or _- with
         * just an underscore
         */
        $str = \str_replace(array('-_', '_-'), '_', $str);

        /**
         * If for some reason the string just became
         * an empty string
         * like maybe all chars were non-alpha numeric, so
         * there we all removed, then we don't want
         * to have an empty string as subject, we'll
         * just say 'topic'
         */
        if (empty($str) || ('-' === $str) || ('_' === $str)) {
            $str = 'Question';
        }

        /**
         * At this point of result string
         * is shorter than the limit, no further
         * processing is necessary, just return it
         */
        if (strlen($str) <= $limit) {

            $str = \trim($str, ' _-');

            return $this->handleReturn($str);
        }

        /**
         * Find the right-most occurance of dash
         */
        $lastPos = \strrpos($str, '-', ($limit - strlen($str)));

        /**
         * If last occurance of dash not found,
         * then we will cut off the string at the $limit lenght
         * This is a rare case when a string did not
         * have any non alphanumeric chars - like when
         * it was a continues string of over 100 chars
         */
        $lastPos = (false !== $lastPos) ? $lastPos : $limit;

        $ret = \substr($str, 0, $lastPos);

        return $this->handleReturn($ret);
    }

    /**
     * Preapre email for more comfortable type
     *
     * @param string $strAddress email address
     *
     * @param string $strFirstName first name
     * @param string $strLastName last name
     *
     * @return string email address string complete with first name, last name and email address
     */
    public static function prepareEmail($strAddress, $strFirstName = '', $strLastName = '') {
        $fn_ln = \trim(\trim($strFirstName) . ' ' . \trim($strLastName));
        $filtered = \htmlspecialchars($fn_ln);
        $name = ('' !== $fn_ln) ? '"' . $filtered . '"' : '';
        $recipient = ('' !== $name) ? $name . ' <' . $strAddress . '>' : $strAddress;

        return $recipient;
    }

    public function toLowerCase() {
        $s = \strtolower($this->string);

        return $this->handleReturn($s);
    }

    public function toUpperCase() {
        $s = \strtoupper($this->string);

        return $this->handleReturn($s);
    }

    public function isEmpty() {
        return (0 === $this->length());
    }

    public function substr($start, $len = null) {
        $s = \substr($this->string, $start, $len);

        return $this->handleReturn($s);
    }

    /**
     * Apply trim() to this string with no
     * extra params passed to trim, which is UTF-8 safe
     * @return object of this class
     */
    public function trim() {
        $s = \trim($this->string);

        return $this->handleReturn($s);
    }

}
